//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.IO;

namespace WorkflowData
{
    /// <summary>
    /// The data for a single point within the HyperCube. This data
    /// represents a table row of values organized according to the
    /// HyperCubeSchema for the parent HyperCube.
    /// 
    /// You can access the data using the [] operator. For example,
    /// if the row of data has 3 values, you can get them using:
    /// 
    /// HyperCubeDataPoint point = hyperCube[0,0,0,0];
    /// double firstValue = (double)point[0];
    /// double secondValue = (double)point[1];
    /// double thirdValue = (double)point[2];
    /// 
    /// A shortcut is to use the form:
    /// double firstValue = (double)hyperCube[0,0,0,0,0];
    /// </summary>
    [Serializable]
    public class HyperCubeDataPoint
    {
        private object[] data;

        // Protected constructor because these should all be created by HyperCube
        internal HyperCubeDataPoint(HyperCube parent)
        {
            if (parent == null || parent.Schema == null)
                throw new ArgumentException("Parent hyper cube must be a valid cube");
            data = new object[parent.Schema.ColumnCount];
        }

        // Overloaded array operator
        public object this[int index]
        {
            get { return data[index]; }
            internal set { data[index] = value; }
        }
    }

    /// <summary>
    /// A hypercube represents set of data stored in a table that is indexed
    /// over four axis. The data is organized according to a HyperCubeSchema.
    /// At each indexed point in the cube there is a table row of data, the
    /// meaning of which can be accessed from the schema.
    /// 
    /// The axis are organized as X, Y, Z, and T (for time). Note that these are
    /// indices and not discrete points. Access along each axis begins at 0 and
    /// continues for the length of the axis.
    /// 
    /// (*[PSPL 1] : Hence minimum length for each axis is 1.)
    /// 
    /// The data is organized as HyperCubeDataPoint objects. This is done so
    /// that you can pull all the data for one indexed point at once. It is also
    /// done to make sure data inserts are done according to the schema for the
    /// cube.
    /// 
    /// You can not create your own HyperCubeDataPoints. It is done for you when
    /// setting data on the cube.
    /// 
    /// The data size of the HyperCube is the multiplied lengths of all four axis.
    /// (*[PSPL 2] : If length of one of the axes is set to "0", data size will be 0.
    /// This supports [PSPL 1] ).
    /// When filled with data this length is then also multiplied by the number of
    /// columns defined by the schema. Thus this data structure can get quite large.
    /// </summary>
    [Serializable]
    public class HyperCube
    {
        #region Axis Enumeration

        /// <summary>
        /// Enum for the Four Axes of the Hyper Cube. These are {X,Y,Z,T}
        /// </summary>
        public enum Axis
        {
            X = 0,
            Y = 1,
            Z = 2,
            T = 3
        }

        #endregion

        #region Member Variables

        private HyperCubeSchema schema;
        private HyperCubeDataPoint[, , ,] data;
        private List<object> xDimValues = null;
        private List<object> yDimValues = null;
        private List<object> zDimValues = null;
        private List<object> tDimValues = null;

        #endregion

        #region Constructors
        /// <summary>
        /// Create a HyperCube according to the parameter schema and with equal
        /// axis lengths amoungst all four axis.
        /// </summary>
        public HyperCube(HyperCubeSchema schema, int axisLength)
            : this(schema, axisLength, axisLength, axisLength, axisLength)
        {
            // Uses referenced constructor
        }

        /// <summary>
        /// Create a HyperCube according to the parameter schema and axis lengths.
        /// Each axis length sets the maximum size for the indices along that axis.
        /// Thus setting the X axis length to 10 for instance will limit you to the
        /// indices of 0 through 9 on that axis.
        /// </summary>
        public HyperCube(HyperCubeSchema schema, int xAxisLength, int yAxisLength, int zAxisLength, int tAxisLength)
        {
            //InitializeCube(schema, xAxisLength, yAxisLength, zAxisLength, tAxisLength);
            if (schema == null)
                throw new ArgumentException("HyperCube schemas must be defined");
            this.schema = schema;
            data = new HyperCubeDataPoint[xAxisLength, yAxisLength, zAxisLength, tAxisLength];
            xDimValues = new List<object>(xAxisLength);
            yDimValues = new List<object>(yAxisLength);
            zDimValues = new List<object>(zAxisLength);
            tDimValues = new List<object>(tAxisLength);
        }



        #endregion

        #region Properties
        /// <summary>
        /// The schema for this cube
        /// </summary>
        public HyperCubeSchema Schema
        {
            get { return schema; }
        }

        /// <summary>
        ///  Since the x,y,z, and t dimensions are indexed, many times the values those indices
        ///  correspond to need to be stored. This list provides a mapping from index to value.
        /// </summary>
        public List<object> XDimensionValues
        {
            get { return this.xDimValues; }
            set { this.xDimValues = value; }
        }

        /// <summary>
        ///  Since the x,y,z, and t dimensions are indexed, many times the values those indices
        ///  correspond to need to be stored. This list provides a mapping from index to value.
        /// </summary>
        public List<object> YDimensionValues
        {
            get { return this.yDimValues; }
            set { this.yDimValues = value; }
        }

        /// <summary>
        ///  Since the x,y,z, and t dimensions are indexed, many times the values those indices
        ///  correspond to need to be stored. This list provides a mapping from index to value.
        /// </summary>
        public List<object> ZDimensionValues
        {
            get { return this.zDimValues; }
            set { this.zDimValues = value; }
        }

        /// <summary>
        ///  Since the x,y,z, and t dimensions are indexed, many times the values those indices
        ///  correspond to need to be stored. This list provides a mapping from index to value.
        /// </summary>
        public List<object> TDimensionValues
        {
            get { return tDimValues; }
            set { this.tDimValues = value; }
        }

        #endregion

        #region Public Methods
        // Overloaded array operator
        public HyperCubeDataPoint this[int xIndex, int yIndex, int zIndex, int tIndex]
        {
            get { return data[xIndex, yIndex, zIndex, tIndex]; }
            protected set
            {
                data[xIndex, yIndex, zIndex, tIndex] = value;
            }
        }

        // Overloaded array operator
        public object this[int xIndex, int yIndex, int zIndex, int tIndex, int colIndex]
        {
            get
            {
                return data[xIndex, yIndex, zIndex, tIndex][colIndex];
            }
            set
            {
                SetData(value, xIndex, yIndex, zIndex, tIndex, colIndex);
            }
        }

        /// <summary>
        /// Returns the axis length of the data this object is currently holding. This value is not related with NcFile object.
        /// </summary>
        /// <param name="axis"></param>
        /// <returns></returns>
        public int GetDataAxisLength(HyperCube.Axis axis)
        {
            return this.data.GetUpperBound((int)axis) + 1;
        }

        /// <summary>
        /// Get the Axis at which Dimension is Mapped.
        /// </summary>
        /// <param name="dimName"></param>
        /// <returns></returns>
        public HyperCube.Axis GetAxis(string dimName)
        {
            if (dimName == this.schema.XDimensionName)
            {
                return Axis.X;
            }
            else if(dimName == this.schema.YDimensionName)
            {
                    return Axis.Y;
            }
            else if(dimName == this.schema.ZDimensionName)
            {
                return Axis.Z;
            }
            else if(dimName == this.schema.TDimensionName)
            {
                return Axis.T;
            }
            throw new ArgumentException("Dimension Name does not exist.");
        }


        /// <summary>
        /// Get the dimension name mapped at axis.
        /// </summary>
        /// <param name="axis"></param>
        /// <returns></returns>
        public string GetDimensionName(HyperCube.Axis axis)
        {
            switch (axis)
            {
                case Axis.X:
                    return this.schema.XDimensionName;
                case Axis.Y:
                    return this.schema.YDimensionName;
                case Axis.Z:
                    return this.schema.ZDimensionName;
                case Axis.T:
                    return this.schema.TDimensionName;
            }
            return "";
        }


        /// <summary>
        /// Retrieves the axis length for one of the four axis of the cube.
        /// This length caps the size of the indices along the axis.
        /// </summary>
        public int GetAxisLength(Axis axis)
        {
            return data.GetLength((int)axis);
        }

        /// <summary>
        /// Set an individual entry in the HyperCube. Since each item is indexed
        /// against four axis and a column defined in the schema, index values
        /// must be passed in.
        /// 
        /// You can perform this same task using the overloaded array operator.
        /// The following two lines of code are equivalent:
        /// 
        /// myCube.SetData(50.0, 1, 2, 1, 2, 0);
        /// myCube[1, 2, 1, 2, 0] = 50.0;
        /// </summary>
        public void SetData(object value, int x, int y, int z, int t, int column)
        {
            // Check the type of the value against the schema's expected type
            Type type = schema.GetColumnType(column);
            if (!type.IsInstanceOfType(value))
                throw new ArgumentException("The type of the value set did not match the schema");

            // Grab the data point. If it is not initialized, do so
            HyperCubeDataPoint point = data[x, y, z, t];
            if (point == null)
            {
                point = new HyperCubeDataPoint(this);
                data[x, y, z, t] = point;
            }

            // Assign the value to the appropriate column
            point[column] = value;
        }

        /// <summary>
        /// This method creates a new HC which is similar to the
        /// this object.
        /// </summary>
        public HyperCube Clone()
        {
            HyperCube newCube = new HyperCube(this.schema, 
                ((null != this.XDimensionValues) ? this.XDimensionValues.Count : 0),
                ((null != this.XDimensionValues) ? this.XDimensionValues.Count : 0),
                ((null != this.XDimensionValues) ? this.XDimensionValues.Count : 0),
                ((null != this.XDimensionValues) ? this.XDimensionValues.Count : 0));

            newCube.XDimensionValues = this.XDimensionValues;
            newCube.YDimensionValues = this.YDimensionValues;
            newCube.ZDimensionValues = this.ZDimensionValues;
            newCube.TDimensionValues = this.TDimensionValues;

            return newCube;
        }

        public override string ToString()
        {
            StringBuilder buff = new StringBuilder();

            buff.AppendLine("HyperCube Schema:\n" + Schema.ToString());

            buff.AppendFormat("Dimension Sizes: X ({0}), Y ({1}), Z ({2}), T ({3})\n", GetDataAxisLength(Axis.X),
                GetDataAxisLength(Axis.Y), GetDataAxisLength(Axis.Z), GetDataAxisLength(Axis.T));
            
            return buff.ToString();
        }

        #endregion
    }
}
